/* Minimal web server with websocket RSSI updates and WiFi config endpoint */
#include <string.h>
#include <stdio.h>
#include "esp_log.h"
#include "esp_http_server.h"
#include "esp_vfs.h"
#include <stdlib.h>
#include "esp_err.h"
#include "esp_wifi.h"
#include "nvs_flash.h"
#include "nvs.h"
#include "cJSON.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/event_groups.h"

static const char *TAG = "web_server";

// If EMBED_TXTFILES is used, the build generates symbols like
// _binary_www_index_html_start/_end. We'll declare them as extern and
// prefer them; fall back to inline strings if embedding isn't available.
// weak externs: if EMBED_TXTFILES created these symbols they'll be available,
// otherwise the pointers will be NULL and we'll fall back to inline strings.
extern const unsigned char _binary_www_index_html_start[] asm("_binary_www_index_html_start") __attribute__((weak));
extern const unsigned char _binary_www_index_html_end[] asm("_binary_www_index_html_end") __attribute__((weak));
extern const unsigned char _binary_www_style_css_start[] asm("_binary_www_style_css_start") __attribute__((weak));
extern const unsigned char _binary_www_style_css_end[] asm("_binary_www_style_css_end") __attribute__((weak));
extern const unsigned char _binary_www_tailwind_min_css_start[] asm("_binary_www_tailwind_min_css_start") __attribute__((weak));
extern const unsigned char _binary_www_tailwind_min_css_end[] asm("_binary_www_tailwind_min_css_end") __attribute__((weak));
extern const unsigned char _binary_www_tailwind_simple_form_css_start[] asm("_binary_www_tailwind_simple_form_css_start") __attribute__((weak));
extern const unsigned char _binary_www_tailwind_simple_form_css_end[] asm("_binary_www_tailwind_simple_form_css_end") __attribute__((weak));
extern const unsigned char _binary_www_chart_min_js_start[] asm("_binary_www_chart_min_js_start") __attribute__((weak));
extern const unsigned char _binary_www_chart_min_js_end[] asm("_binary_www_chart_min_js_end") __attribute__((weak));

// Inline fallbacks (kept from earlier implementation)
static const char index_html[] =
"<!doctype html>\n"
"<html>\n"
"<head>\n"
"  <meta charset=\"utf-8\">\n"
"  <meta name=\"viewport\" content=\"width=device-width,initial-scale=1\">\n"
"  <title>WiFi Config</title>\n"
"  <link rel=\"stylesheet\" href=\"/tailwind.min.css\">\n"
"  <link rel=\"stylesheet\" href=\"/tailwind-simple-form.css\">\n"
"  <link rel=\"stylesheet\" href=\"/style.css\">\n"
"</head>\n"
"<body class=\"bg-gray-100 p-6\">\n"
"  <div class=\"max-w-3xl mx-auto\">\n"
"    <h1 class=\"text-2xl font-bold mb-4\">WiFi 配置</h1>\n"
"    <form id=\"wifiForm\" class=\"simple-form\">\n"
"      <label>SSID\n"
"        <input type=\"text\" name=\"ssid\" id=\"ssid\" placeholder=\"SSID\" />\n"
"      </label>\n"
"      <label>密码\n"
"        <input type=\"password\" name=\"password\" id=\"password\" placeholder=\"Password\" />\n"
"      </label>\n"
"      <div>\n"
"        <button type=\"submit\">保存并连接</button>\n"
"      </div>\n"
"    </form>\n"
"\n"
"    <h2 class=\"text-xl font-semibold mt-8\">RSSI 变化 (每10秒更新)</h2>\n"
"    <canvas id=\"rssiChart\" width=\"600\" height=\"300\"></canvas>\n"
"  </div>\n"
"\n"
"  <script src=\"/chart.min.js\"></script>\n"
"  <script>\n"
"    const ctx = document.getElementById('rssiChart').getContext('2d');\n"
"    const data = { labels: [], datasets: [{ label: 'RSSI (dBm)', data: [], borderColor: 'rgb(75, 192, 192)', tension: 0.1 }] };\n"
"    const config = { type: 'line', data };\n"
"    const rssiChart = new Chart(ctx, config);\n"
"\n"
"    let ws;\n"
"    function startWS() {\n"
"      ws = new WebSocket(`ws://${location.host}/ws`);\n"
"      ws.onopen = () => console.log('ws open');\n"
"      ws.onmessage = (evt) => {\n"
"        try { const obj = JSON.parse(evt.data); const time = new Date().toLocaleTimeString(); data.labels.push(time); data.datasets[0].data.push(obj.rssi); if (data.labels.length > 20) { data.labels.shift(); data.datasets[0].data.shift(); } rssiChart.update(); } catch(e) { console.error(e); }\n"
"      };\n"
"      ws.onclose = () => setTimeout(startWS, 2000);\n"
"    }\n"
"    startWS();\n"
"\n"
"    document.getElementById('wifiForm').addEventListener('submit', async (e) => {\n"
"      e.preventDefault();\n"
"      const ssid = document.getElementById('ssid').value;\n"
"      const password = document.getElementById('password').value;\n"
"      const resp = await fetch('/configure', { method: 'POST', headers: {'Content-Type':'application/json'}, body: JSON.stringify({ ssid, password }) });\n"
"      const j = await resp.json();\n"
"      alert('保存: ' + j.status);\n"
"    });\n"
"  </script>\n"
"</body>\n"
"</html>\n";

static const char style_css[] =
"body { font-family: Arial, Helvetica, sans-serif; }\n"
".simple-form label { display:block; margin-bottom:8px; }\n"
".simple-form input { width:100%; padding:8px; margin-top:4px; }\n"
".simple-form button { margin-top:8px; padding:8px 12px; background:#2563eb; color:#fff; border:none; border-radius:4px; }\n";

static const char tailwind_min_css[] =
".text-2xl{font-size:1.5rem;} .font-bold{font-weight:700} .mb-4{margin-bottom:1rem} .bg-gray-100{background-color:#f7fafc} .p-6{padding:1.5rem} .max-w-3xl{max-width:48rem} .mx-auto{margin-left:auto;margin-right:auto} .text-xl{font-size:1.25rem} .font-semibold{font-weight:600} .mt-8{margin-top:2rem} .border{border:1px solid #e5e7eb} .rounded{border-radius:0.375rem}";

static const char tailwind_simple_form_css[] =
".simple-form { background:#fff; padding:16px; border-radius:6px; box-shadow:0 1px 3px rgba(0,0,0,0.08); } .simple-form label { font-weight:600; }";

static const char chart_min_js[] =
"window.Chart = function(ctx, config) { const chart = { data: config.data, update: function() {} }; return chart; };";

// Simple helper to serve embedded file
// Serve either an embedded buffer (ptr+len) or a nul-terminated C string fallback.
static esp_err_t serve_embedded_buf(httpd_req_t *req, const void *ptr, size_t len, const char *fallback, const char *content_type)
{
    httpd_resp_set_type(req, content_type);
    httpd_resp_set_hdr(req, "Cache-Control", "no-cache");
    if (ptr != NULL && len > 0) {
        return httpd_resp_send(req, (const char *)ptr, len);
    }
    if (fallback) {
        return httpd_resp_send(req, fallback, strlen(fallback));
    }
    return httpd_resp_send_404(req);
}

// Handlers for embedded resources
static esp_err_t index_get_handler(httpd_req_t *req)
{
    int sockfd = httpd_req_to_sockfd(req);
    ESP_LOGI(TAG, "HTTP GET / request, sockfd=%d", sockfd);
    const void *ptr = NULL; size_t len = 0;
    if (&_binary_www_index_html_start != NULL && &_binary_www_index_html_end != NULL) {
        ptr = _binary_www_index_html_start;
        len = (size_t)(_binary_www_index_html_end - _binary_www_index_html_start);
    }
    return serve_embedded_buf(req, ptr, len, index_html, "text/html");
}

static esp_err_t style_get_handler(httpd_req_t *req)
{
    const char *uri = req->uri;
    int sockfd = httpd_req_to_sockfd(req);
    ESP_LOGI(TAG, "HTTP GET %s request, sockfd=%d", uri, sockfd);
    if (strcmp(uri, "/style.css") == 0) {
        const void *ptr = NULL; size_t len = 0;
        if (&_binary_www_style_css_start != NULL && &_binary_www_style_css_end != NULL) {
            ptr = _binary_www_style_css_start;
            len = (size_t)(_binary_www_style_css_end - _binary_www_style_css_start);
        }
        return serve_embedded_buf(req, ptr, len, style_css, "text/css");
    } else if (strcmp(uri, "/tailwind.min.css") == 0) {
        const void *ptr = NULL; size_t len = 0;
        if (&_binary_www_tailwind_min_css_start != NULL && &_binary_www_tailwind_min_css_end != NULL) {
            ptr = _binary_www_tailwind_min_css_start;
            len = (size_t)(_binary_www_tailwind_min_css_end - _binary_www_tailwind_min_css_start);
        }
        return serve_embedded_buf(req, ptr, len, tailwind_min_css, "text/css");
    } else if (strcmp(uri, "/tailwind-simple-form.css") == 0) {
        const void *ptr = NULL; size_t len = 0;
        if (&_binary_www_tailwind_simple_form_css_start != NULL && &_binary_www_tailwind_simple_form_css_end != NULL) {
            ptr = _binary_www_tailwind_simple_form_css_start;
            len = (size_t)(_binary_www_tailwind_simple_form_css_end - _binary_www_tailwind_simple_form_css_start);
        }
        return serve_embedded_buf(req, ptr, len, tailwind_simple_form_css, "text/css");
    }
    return httpd_resp_send_404(req);
}

static esp_err_t chart_js_get_handler(httpd_req_t *req)
{
    int sockfd = httpd_req_to_sockfd(req);
    ESP_LOGI(TAG, "HTTP GET /chart.min.js request, sockfd=%d", sockfd);
    const void *ptr = NULL; size_t len = 0;
    if (&_binary_www_chart_min_js_start != NULL && &_binary_www_chart_min_js_end != NULL) {
        ptr = _binary_www_chart_min_js_start;
        len = (size_t)(_binary_www_chart_min_js_end - _binary_www_chart_min_js_start);
    }
    return serve_embedded_buf(req, ptr, len, chart_min_js, "application/javascript");
}

static esp_err_t configure_post_handler(httpd_req_t *req)
{
    int total_len = req->content_len;
    if (total_len <= 0 || total_len > 1024) {
        httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "invalid content length");
        return ESP_FAIL;
    }
    char *buf = malloc(total_len + 1);
    if (!buf) {
        httpd_resp_send_500(req);
        return ESP_FAIL;
    }
    int ret = httpd_req_recv(req, buf, total_len);
    if (ret <= 0) {
        free(buf);
        httpd_resp_send_500(req);
        return ESP_FAIL;
    }
    buf[ret] = '\0';
    ESP_LOGI(TAG, "Received config: %s", buf);

    cJSON *root = cJSON_Parse(buf);
    free(buf);
    if (!root) {
        ESP_LOGW(TAG, "cJSON_Parse failed");
        httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "invalid json");
        return ESP_FAIL;
    }
    const cJSON *js_ssid = cJSON_GetObjectItemCaseSensitive(root, "ssid");
    const cJSON *js_password = cJSON_GetObjectItemCaseSensitive(root, "password");
    if (!cJSON_IsString(js_ssid) || (js_ssid->valuestring == NULL)) {
        cJSON_Delete(root);
        httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "missing ssid");
        return ESP_FAIL;
    }

    const char *ssid = js_ssid->valuestring;
    const char *password = cJSON_IsString(js_password) && js_password->valuestring ? js_password->valuestring : "";

    // Save to NVS
    nvs_handle_t nvs_handle;
    esp_err_t err = nvs_open("wifi", NVS_READWRITE, &nvs_handle);
    if (err == ESP_OK) {
        err = nvs_set_str(nvs_handle, "ssid", ssid);
        if (err != ESP_OK) {
            ESP_LOGW(TAG, "nvs_set_str ssid failed: %d", err);
        }
        err = nvs_set_str(nvs_handle, "password", password);
        if (err != ESP_OK) {
            ESP_LOGW(TAG, "nvs_set_str password failed: %d", err);
        }
        err = nvs_commit(nvs_handle);
        if (err != ESP_OK) {
            ESP_LOGW(TAG, "nvs_commit failed: %d", err);
        }
        nvs_close(nvs_handle);
    } else {
        ESP_LOGW(TAG, "nvs_open failed: %d", err);
    }

    // Apply wifi config
    wifi_config_t cfg = {0};
    strncpy((char *)cfg.sta.ssid, ssid, sizeof(cfg.sta.ssid)-1);
    strncpy((char *)cfg.sta.password, password, sizeof(cfg.sta.password)-1);
    ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA));
    ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &cfg));
    ESP_LOGI(TAG, "Set new STA config SSID=%s", ssid);
    esp_err_t e = esp_wifi_connect();
    if (e != ESP_OK) {
        ESP_LOGW(TAG, "esp_wifi_connect returned %d", e);
    }

    cJSON_Delete(root);

    httpd_resp_set_type(req, "application/json");
    const char *resp = "{\"status\":\"ok\"}";
    httpd_resp_send(req, resp, strlen(resp));
    return ESP_OK;
}

// WebSocket handling: when a client connects, create a task to push RSSI every 10s
typedef struct {
    httpd_handle_t hd;
    int sockfd;
} ws_client_t;

static void ws_rssi_task(void *arg)
{
    ws_client_t *client = (ws_client_t *)arg;
    char sendbuf[64];

    ESP_LOGI(TAG, "ws_rssi_task started for sockfd=%d", client->sockfd);
    // short delay to allow handshake to fully settle on some stacks
    vTaskDelay(pdMS_TO_TICKS(200));

    // try an initial test message to verify the socket is usable
    snprintf(sendbuf, sizeof(sendbuf), "{\"rssi\":%d,\"test\":1}", 0);
    httpd_ws_frame_t test_frame = {
        .final = true,
        .fragmented = false,
        .len = (int)strlen(sendbuf),
        .type = HTTPD_WS_TYPE_TEXT,
        .payload = (uint8_t *)sendbuf
    };
    esp_err_t tres = httpd_ws_send_data(client->hd, client->sockfd, &test_frame);
    if (tres != ESP_OK) {
        ESP_LOGW(TAG, "initial ws send failed: %d (%s)", tres, esp_err_to_name(tres));
        // continue — sometimes initial sends can fail transiently; we'll attempt periodic sends
    } else {
        ESP_LOGI(TAG, "initial ws send ok (sockfd=%d)", client->sockfd);
    }

    while (1) {
        // get RSSI
        wifi_ap_record_t apinfo;
        int rssi = 0;
        esp_err_t rc = esp_wifi_sta_get_ap_info(&apinfo);
        if (rc == ESP_OK) {
            rssi = apinfo.rssi;
            ESP_LOGI(TAG, "Read RSSI=%d", rssi);
        } else {
            ESP_LOGW(TAG, "esp_wifi_sta_get_ap_info failed: %d (%s)", rc, esp_err_to_name(rc));
        }

        int len = snprintf(sendbuf, sizeof(sendbuf), "{\"rssi\":%d}", rssi);
        httpd_ws_frame_t wsframe = {
            .final = true,
            .fragmented = false,
            .len = len,
            .type = HTTPD_WS_TYPE_TEXT,
            .payload = (uint8_t *)sendbuf
        };
        esp_err_t res = httpd_ws_send_data(client->hd, client->sockfd, &wsframe);
        if (res != ESP_OK) {
            ESP_LOGW(TAG, "ws send failed: %d (sockfd=%d) (%s)", res, client->sockfd, esp_err_to_name(res));
            // assume connection closed or broken — exit task
            break;
        }
        ESP_LOGI(TAG, "ws send ok: payload=%.*s", len, sendbuf);

        vTaskDelay(pdMS_TO_TICKS(10000));
    }

    free(client);
    vTaskDelete(NULL);
}

static esp_err_t ws_handler(httpd_req_t *req)
{
    int sockfd = httpd_req_to_sockfd(req);
    ESP_LOGI(TAG, "ws_handler called: method=%d uri=%s sockfd=%d", req->method, req->uri, sockfd);
    if (req->method == HTTP_GET) {
        // upgrade to websocket
        httpd_ws_frame_t ws_pkt;
        memset(&ws_pkt, 0, sizeof(ws_pkt));
        // accept connection and get socket fd
        int sockfd = httpd_req_to_sockfd(req);
        ws_client_t *client = calloc(1, sizeof(ws_client_t));
        client->hd = req->handle;
        client->sockfd = sockfd;
        // Do an initial send to verify the socket is usable before spawning the task
        const char *hello = "{\"rssi\":0}";
        httpd_ws_frame_t hello_frame = {
            .final = true,
            .fragmented = false,
            .len = (int)strlen(hello),
            .type = HTTPD_WS_TYPE_TEXT,
            .payload = (uint8_t *)hello
        };
        esp_err_t r = httpd_ws_send_data(client->hd, client->sockfd, &hello_frame);
        if (r != ESP_OK) {
            ESP_LOGW(TAG, "ws initial send failed: %d (%s)", r, esp_err_to_name(r));
            free(client);
            return ESP_FAIL;
        }
        // create task to push RSSI
        xTaskCreate(ws_rssi_task, "ws_rssi", 4*1024, client, tskIDLE_PRIORITY+1, NULL);
        return ESP_OK;
    }
    return ESP_FAIL;
}

static httpd_uri_t index_uri = {
    .uri       = "/",
    .method    = HTTP_GET,
    .handler   = index_get_handler,
    .user_ctx  = NULL
};

static httpd_uri_t chart_js_uri = {
    .uri       = "/chart.min.js",
    .method    = HTTP_GET,
    .handler   = chart_js_get_handler,
    .user_ctx  = NULL
};

static httpd_uri_t configure_uri = {
    .uri       = "/configure",
    .method    = HTTP_POST,
    .handler   = configure_post_handler,
    .user_ctx  = NULL
};

static httpd_uri_t ws_uri = {
    .uri       = "/ws",
    .method    = HTTP_GET,
    .handler   = ws_handler,
    .user_ctx  = NULL
};

httpd_handle_t start_webserver(void)
{
    httpd_config_t config = HTTPD_DEFAULT_CONFIG();
    config.stack_size = 8192;
    httpd_handle_t server = NULL;

    ESP_LOGI(TAG, "Starting web server");
    if (httpd_start(&server, &config) == ESP_OK) {
        httpd_register_uri_handler(server, &index_uri);
        httpd_register_uri_handler(server, &chart_js_uri);
        httpd_register_uri_handler(server, &configure_uri);
        httpd_register_uri_handler(server, &ws_uri);
        // register CSS routes explicitly
        httpd_register_uri_handler(server, &(httpd_uri_t){ .uri = "/style.css", .method = HTTP_GET, .handler = style_get_handler });
        httpd_register_uri_handler(server, &(httpd_uri_t){ .uri = "/tailwind.min.css", .method = HTTP_GET, .handler = style_get_handler });
        httpd_register_uri_handler(server, &(httpd_uri_t){ .uri = "/tailwind-simple-form.css", .method = HTTP_GET, .handler = style_get_handler });
        return server;
    }

    ESP_LOGI(TAG, "Failed to start web server");
    return NULL;
}

void stop_webserver(httpd_handle_t server)
{
    if (server) {
        httpd_stop(server);
    }
}

// start server task
void web_server_start_task(void *arg)
{
    // wait a bit for wifi to be ready
    vTaskDelay(pdMS_TO_TICKS(2000));
    start_webserver();
    vTaskDelete(NULL);
}

void start_web_services()
{
    xTaskCreate(web_server_start_task, "web_server", 6*1024, NULL, tskIDLE_PRIORITY+1, NULL);
}
